/*
 * Copyright 2013 Krzysztof Smigielski
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.nilcaream.dirstore.core.common;

import com.google.common.base.Charsets;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.hash.Hashing;
import com.nilcaream.dirstore.core.model.ArchiveElement;
import com.nilcaream.dirstore.core.model.ContentId;
import com.nilcaream.dirstore.core.model.TrimmedElement;

import javax.inject.Singleton;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import static com.google.common.base.Strings.isNullOrEmpty;
import static com.nilcaream.dirstore.core.common.Utils.calculateCrc32;
import static com.nilcaream.dirstore.core.common.Utils.getEntries;

/**
 * TODO
 * <p/>
 * Krzysztof Smigielski 2/9/13 1:52 PM
 */
@Singleton
public class ZipTool {

    private static final int BUFFER_SIZE = 256 * 1024;

    public ZipOutputStream createZipOutput(OutputStream outputStream) {
        ZipOutputStream zipOut = new ZipOutputStream(outputStream);
        zipOut.setLevel(9);
        return zipOut;
    }

    public void add(ArchiveElement archiveElement, TrimmedElement trimmedElement, ZipOutputStream zip) throws IOException {
        ZipEntry zipEntry = createEntry(archiveElement, trimmedElement);
        addEntry(trimmedElement.getData(), zip, zipEntry);
    }

    private ZipEntry createEntry(ArchiveElement archiveElement, TrimmedElement trimmedElement) throws IOException {
        ZipEntry zipEntry = new ZipEntry(archiveElement.getName());
        zipEntry.setTime(archiveElement.getLastModified());
        if (!archiveElement.isDirectory()) {
            setComment(zipEntry, trimmedElement.getComment(), calculateCrc32(new File(archiveElement.getUri())), archiveElement.getOriginalSize());
        }

        if (trimmedElement.isCompressionNeeded()) {
            zipEntry.setMethod(ZipEntry.DEFLATED);
        } else {
            zipEntry.setMethod(ZipEntry.STORED);
        }
        return zipEntry;
    }

    private void setComment(ZipEntry entry, String comment, long crc32, long originalSize) {
        StringBuilder sb = new StringBuilder(100);
        sb.append(comment);
        sb.append(" c:").append(Long.toHexString(crc32).toUpperCase());
        sb.append(" s:").append(originalSize);
        entry.setComment(sb.toString().trim());
    }

    private void addEntry(byte[] bytes, ZipOutputStream output, ZipEntry zipEntry) throws IOException {
        setCrcAndSize(zipEntry, bytes);
        output.putNextEntry(zipEntry);
        output.write(bytes);
        output.closeEntry();
    }

    private void setCrcAndSize(ZipEntry zipEntry, byte[] bytes) {
        zipEntry.setSize(bytes.length);
        CRC32 crc32 = new CRC32();
        crc32.update(bytes);
        zipEntry.setCrc(crc32.getValue());
    }

    public void add(ZipEntry source, TrimmedElement trimmedElement, ZipOutputStream zip) throws IOException {
        ZipEntry zipEntry = createEntry(source, trimmedElement);
        addEntry(trimmedElement.getData(), zip, zipEntry);
    }

    private ZipEntry createEntry(ZipEntry source, TrimmedElement trimmedElement) {
        ZipEntry result = new ZipEntry(source.getName());
        result.setTime(source.getTime());
        if (!source.isDirectory()) {
            setComment(result, trimmedElement.getComment(), source.getCrc(), source.getSize());
        }

        if (trimmedElement.isCompressionNeeded()) {
            result.setMethod(ZipEntry.DEFLATED);
        } else {
            result.setMethod(ZipEntry.STORED);
        }
        return result;
    }

    /*
    public Multimap<String, String> createDirectoryToContentsMap(ZipFile rootZip) throws IOException {
        Set<String> rootDirectories = new HashSet<>(100);
        rootDirectories.add("");
        for (ZipEntry zipEntry : getEntries(rootZip)) {
            if (zipEntry.isDirectory()) {
                rootDirectories.add(zipEntry.getName());
            }
        }

        Multimap<String, String> directoryToFiles = ArrayListMultimap.create(rootDirectories.size(), 100);
        for (ZipEntry zipEntry : getEntries(rootZip)) {
            String file = zipEntry.getName();
            for (String directory : rootDirectories) {
                if (file.startsWith(directory) && !file.equals(directory)) {
                    String r = file.substring(directory.length());
                    directoryToFiles.put(directory, "r:" + r + " " + zipEntry.getComment());
                }
            }
        }
        return directoryToFiles;
    }
    */

    public Map<String, ContentId> createDirectoryToContentIdMap(ZipFile rootZip) throws IOException {
        Map<String, ContentId> dirToContentId = Maps.newHashMapWithExpectedSize(rootZip.size());

        dirToContentId.put("", createContentId(rootZip, ""));
        for (ZipEntry zipEntry : getEntries(rootZip)) {
            if (zipEntry.isDirectory()) {
                dirToContentId.put(zipEntry.getName(), createContentId(rootZip, zipEntry.getName()));
            }
        }
        return dirToContentId;
    }

    private ContentId createContentId(ZipFile rootZip, String directory) {
        List<String> contents = Lists.newArrayListWithExpectedSize(1000);
        long size = 0;

        for (ZipEntry zipEntry : getEntries(rootZip)) {
            String name = zipEntry.getName();
            if (name.startsWith(directory) && !name.equals(directory)) {
                String relativePath = name.substring(directory.length());
                contents.add(("r:" + relativePath + " " + zipEntry.getComment()).trim());
                if (!zipEntry.isDirectory()) {
                    size += extractNonTrimmedFileSize(zipEntry);
                }
            }
        }

        Collections.sort(contents);

        String id = Hashing.sha256().hashString(toString(contents), Charsets.UTF_8).toString();
        return new ContentId(id, size);
    }

    private String toString(List<String> contents) {
        StringBuilder sb = new StringBuilder(contents.size() * 80);
        for (String content : contents) {
            sb.append(content);
        }
        return sb.toString();
    }

    public long getDirectorySize(String zipFileName, String directoryName) throws IOException {
        long size = 0;
        try (ZipFile zipFile = new ZipFile(new File(zipFileName), ZipFile.OPEN_READ, Charsets.UTF_8)) {
            for (ZipEntry zipEntry : getEntries(zipFile)) {
                if (!zipEntry.isDirectory() && zipEntry.getName().startsWith(directoryName)) {
                    size += extractNonTrimmedFileSize(zipEntry);
                }
            }
        }
        return size;
    }

    private long extractNonTrimmedFileSize(ZipEntry zipEntry) {
        Preconditions.checkState(!isNullOrEmpty(zipEntry.getComment()), zipEntry + " zip entry does not contain valid comment field");
        String[] comments = zipEntry.getComment().split(" ");
        for (String comment : comments) {
            if (comment.startsWith("s:")) {
                return Long.parseLong(comment.substring(2));
            }
        }
        throw new IllegalStateException(zipEntry + " zip entry does not contain valid comment field");
    }
}
